C++ Primer 第2章 变量和基本类型

2.1 基本内置类型

C++定义了一套包括算术类型和空类型在内的基本数据类型。

2.1.1 算术类型

算术类型分为两类:整型和浮点型。算术类型的尺寸在不同机器上有所差别。

类型含义最小尺寸常见
bool布尔类型未定义
char字符8位
wchar_t宽字符16位
char16_tUnicode字符16位
char32_tUnicode字符32位
short短整型16位
int整型16位
long长整型32位
long long长整型64位
float单精度浮点数6位有效数字32位
double双精度浮点数10位有效数字64位
long double扩展精度浮点数10位有效数字96或128位

除去布尔型和扩展的字符型之外,其它整型可以划分为带符号的无符号的两种。

类型int、short、long和long long都是带符合的,通常在类型名前添加unsigned就可以得到无符号类型。

字符型被分为了三种:char、signed char和unsifned char。尽管字符型有三种,但是字符的表现形式却只有两种:带符号的和无符号的。类型char实际上会表现为上述两种形式中的一种,具体是哪种由编译器决定。

C++标准没有规定带符号类型应如何表示,但是约定了在表示范围内正值和负值的量应该平衡。

在算术表达式中不要使用char或bool,只有在存放字符或布尔值时才使用它们。因为类型char在一些机器上是有符号的,在另一些机器上是无符号的。

执行浮点数运算选用double,因为float通常精度不够而双精度浮点数和单精度浮点数的计算代价相差无几,甚至double比float快。

2.1.2 类型转换

类型所能表示的值的范围决定了转换的过程:

  • 当我们将非布尔型的算术值赋值给布尔类型时,初始值为0则结果为false,否则为true

  • 当我们将布尔值赋值给非布尔型时,初始值为false则结果为0,初始值为true结果为1

  • 将浮点数赋值给整数类型,保留浮点数小数点前的部分

  • 将整数值赋值给浮点型时,小数部分记为0。如果该整数所占的空间超过了浮点类型的容量,精度可能有损失

  • 当我们赋给无符号类型一个超过它表示范围的值时,结果是初始值对无符号类型表示值总数取模后的余数。

    8位的unsigned char表示0到255区间的数,则表示值总数为256

  • 当我们赋给带符号类型一个超出它表示范围的值时,结果是未定义的。

如果表达式里既有带符号类型又有无符号类型,带符号类型会转换成无符号类型。

2.1.3 字面值常量

整型字面值的表示方法:

  • 十进制:20
  • 八进制:024
  • 十六进制:0x14

整型字面值的数据类型:

  • 默认情况下,十进制字面值是带符号数,八进制和十六进制字面值既可能是带符号的也可能是无符号的
  • 十进制字面值的类型是int、long和long long中能容纳其数值值的最小尺寸的那个
  • 八进制和十六进制字面值的类型是能容纳其数值的int、unsinged int、long、unsigned long、long long和unsigned long long中尺寸最小者
  • 十进制字面值可以是带符号类型,严格说十进制字面值不会是负数,负号并不在字面值之内,仅仅是对字面值取负值而已

浮点型字面值的表示方法:

  • 小数表示:3.14159
  • 科学计数法:3.14159E0、0e0

浮点型字面值的类型:double

字符和字符串字面值:

  • 由单引号括起来的一个字符称为char型字面值:‘a’
  • 由双引号括起来的零个或多个字符则构成字符串型字面值:“Hello”

字符串字面值的类型实际上是由常量字符构成的数组,编译器在每个字符串的结尾处添加一个空字符(‘\0’),因此字符串字面值的实际长度比内容多1。

如果两个字符串字面值位置紧邻且仅由空格、缩进和换行符分隔,则它们实际上是一个整体。

std::cout << "a really, really"
            "two lines" << std::endl;

有两类字符不能直接使用:

  1. 不可打印字符:退格或其他控制字符
  2. 特殊含义字符:单引号、双引号、问号、反斜线

以上两种情况需要用的转义序列:以反斜线开始+字符

可通过添加类型前缀和后缀改变字面值默认类型:

  • 字符和字符串字面值通过设置前缀
  • 整型和浮点型字面值通过设置后缀

2.2 变量

变量提供一个具名的、可供程序操作的存储空间。

2.2.1 变量定义

区分对象和变量:遵循多数人习惯,将具有某种数据类型的内存空间认为是对象

当对象在创建时获得一个特定的值,则成对象被初始化了。

初始化不是赋值,初始化的含义是创建变量时赋予其一个初始值,而赋值是把对象的当前值擦除,而以一个新值来替代。

列表初始化

变量初始化有四种形式:

int a = 0;
int a = {0};
int a{0};
int a(0);

C++11开始花括号初始化变量得到了全面应用,这种初始化形式称为列表初始化。

当用于内置类型的变量时,列表初始化有个重要特点:如果我们使用列表初始化且初始值存在丢失信息的风险,编译器将报错。

long double ld = 3.1415926536;
int a{ld}, b = {ld};    //错误,因为存在丢失信息的风险
int d = ld;        //正确,且确实丢失部分值

默认初始化,变量没有指定处值将被赋予默认值,由变量类型决定,且受定义变量位置影响:

  • 定义于任何函数体之外的变量被初始化为0
  • 函数体内部的内置类型变量不被初始化,未被初始化的内置类型变量的值是未定义的

2.2.2 变量声明和定义的关系

为了允许把程序拆分成多个逻辑部分来编写,C++语言支持分离式编译机制,该机制允许将程序分割为若干个文件,每个文件可悲独立编译。

为了支持分离式编译,C++将声明和定义区分开来:

  • 声明:使得名字为程序所知,一个文件如果使用别处定义的名字则必须包含对那个名字的声明。
  • 定义:负责创建与名字关联的实体,申请存储空间,可能还会赋初值

如果想要声明一个变量而非定义它,就在变量名添加关键字extern,并不要显示地初始化变量。【任何包含了显示初始化的声明即成为定义】

extern int i;    //声明i而非定义i
int j;            //声明并定义j
extern double pi = 3.1416;    //定义

在函数体内部如果试图初始化一个由extern关键字标记的变量,将引发错误。

变量只能定义一次,可以多次声明。

C++是一种静态类型的语言,其含义是编译阶段检查类型,其过程称为类型检查。

2.2.3 标识符

标识符由字母、数字或下划线组成,必须以字母或下划线开头。标识符对长度无限制,对大小写敏感。

  • C++语言保留一些名字供语言本身使用,不可用作标识符
  • C++为标准库保留一些名字,用户定义的标识符不能连续出现两个下划线开头,不能出现下划线紧连大写字母开头
  • 定义在函数体外的标识符不能以下划线开头

变量命名规范:

  • 标识符要体现实际含义
  • 变量名一般使用小写字母
  • 用户自定义的类名一般使用大写字母开头
  • 如果标识符由多个单词组成,可以用下划线分隔或驼峰命名:student_loan、studentLoan

2.2.4 名字的作用域

作用域是程序的一部分,大多数作用域都以花括号分隔。同一个名字在不同的作用域可能指向不同的实体。

名字main定义于所有花括号之外,和其它大多数定义在函数体之外的名字一样拥有全局作用域。全局作用域的名字在整个程序的范围内都可使用。定义于函数体内,则拥有块作用域,访问范围仅限块内。

作用域能彼此包含,被包含的作用域称为内层作用域,包含着别的作用域的作用域称为外层作用域。

2.3 复合类型

复合类型:基于其他类型定义的类型。引用和指针属于复合类型。

2.3.1 引用

引用:左值引用,为对象起了另一个名字。定义引用时,程序将它和它的初始值对象绑定在一起,并无法重新绑定到另一个对象,因此引用必须初始化。

引用并非对象,是一个已经存在的对象的别名,因为引用本身不是对象,所以无法定义引用的引用。

引用标识符都必须以&开头:

int i = 1024;
int &r = i;

2.3.2 指针

指针是”指向“另一种类型的复合类型。与引用类似,指针也实现了对其他对象的间接访问。

指针和引用的不同点:

  1. 指针本身就是一个对象,允许指针赋值和拷贝,可以修改指向
  2. 指针无须定义时赋初值

指针存放某个对象的地址,要获取该地址,需要使用取地址符(操作符&)

int ival = 42;
int *p = &ival;

引用不是对象,无法定义指向引用的指针。指针的类型与指向对象的类型必须相同。

指针的值应属于4种状态之一:

  1. 指向一个对象
  2. 指向紧邻对象所占空间的下一个位置
  3. 空指针 ,意味着没有指向对象
  4. 无效指针,上述情况之外的其他值

解引用符(操作符*):对指针解引用会得出所指的对象,因此对解引用的结果赋值,实际上也就是给指针所指对象赋值。解引用操作仅适用于哪些确实指向了某个对象的有效指针

空指针不指向任何对象,生成空指针的方法:

int *p1 = nullptr;    //等价于int *p1 = 0;
int *p2 = 0;        //直接初始化为字面常量0
//需要首先#include cstdlib
int *p3 = NULL;        //等价于int *p3 = 0;

nullptr是C++11后引入的方法,nullptr是一种特殊类型的字面值,可以转换成任意其他指针类型。NULL是预处理变量,它的值是0。

对于两个类型相同的合法指针,可以进行比较,比较结果是bool类型:如果指针存放的地址值相同则相等。

void*指针:特殊类型的指针,可用于存放任意对象的地址。不能直接操作void*指针所指的对象,因为我们不知道这个对象到底是什么类型,也无法确定能做哪些操作。

2.3.3 理解复合类型的声明

在同一条定义语句中,虽然基本数据类型只有一个,但是声明符的形式却可以不同,也就可以定义出不同类型的变量:

int i = 1024, *p = &i, &r = i;

建议将操作符*(或是&)与变量名连在一起,强调变量具有的复合类型。

一般来说,声明符中修饰符的个数没有限制,当多个修饰符连写在一起时,按照其逻辑憨详加解释即可。**表示指向指针的指针,***表示指向指针的指针的指针,以此类推。

引用本身不是对象,不能定义指向引用的指针。

指针是对象,存在对指针的引用。

int i = 42;
int *p;
int *&r = p;    //对指针p的引用

r = &i;
*r = 0;        //i的值变为0

理解r的类型是什么:从右向左阅读r的定义:离变量名最近的符号对变量的类型有直接影响,所以r是一个引用。声明符的其余部分用以确定r引用的类型是什么,所以r引用的类型是一个int指针。

2.4 const限定符

const限定符对变量的类型加以限定,防止其值被修改。

因为const对象一旦创建后不能再修改,所以const对象必须初始化。

const int i = get_size();    //正确:运行时初始化
const int j = 42;            //正确:编译时初始化
const int k;                //错误:未经初始化

const类型的对象上不能执行改变其内容的操作。const限定符仅仅在执行改变其值的操作时才发挥作用。

int i = 42;
const int ci = i;    //正确:i的值拷贝给ci
int j = ci;            //正确:ci的值拷贝给j

默认情况下,const对象被设定为仅在文件内有效,当多个文件出现了同名const变量时,其实等同于在不同文件中分别定义了独立的变量。

若想要多个文件共享一个const变量,则不管声明还是定义都添加extern关键字,这样只需定义一次就可以了。

//定义并初始化一个常量,该常量能被其他文件访问
extern const int bufSize = fcn();
//在另一个文件声明使用
extern const int buffSize;

2.4.1 const的引用

可以把引用绑定到const对象上,称为对常量的引用,对常量的引用不能修改它所绑定的对象。

const int ci = 1024;
const int &r1 = ci;    //正确,引用及其对应的对象都是常量
r1 = 42//错误:r1是对常量的引用
int &r2 = ci;    //错误:试图让一个非常量引用指向一个常量对象

引用的类型必须与其所引用的对象的类型一致,但是有两个例外:

  1. 在初始化常量引用时,允许用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可
  2. 允许为一个常量引用绑定非常量对象、字面值,甚至是个一般表达式
int i = 42;
const int &r1 = i;        //正确
const int &r2 = 42;        //正确
cosnt int &r3 = r1 * 2;    //正确
int &r4 = r1 * 2;        //错误

能够这样的原因,是产生了一个临时变量,临时变量通过绑定的对象进行初始化,接着绑定到引用上。如此,如果引用类型不是const,则可以通过引用修改到临时变量,这是非法的。

2.4.2 指针和const

指向常量的指针:存放常量对象的地址。

指针类型必须与其所指对象的类型一致,但是有两个例外:

  1. 允许另一个指向常量的指针指向一个非常量对象

    指向常量的指针仅仅要求不能通过该指针改变对象的值,而没有规定那个对象的值不能通过其他途径改变

  2. 占位

指针是对象而引用不是,允许把指针本身定为常量。常量指针必须初始化,一旦初始化完成就不能改变。常量指针不变的是指针本身的值而不是指向的那个值。

弄清楚声明的含义:从右向左阅读,离变量名最近的对变量有直接影响,其余部分确定对象的类型

2.4.3 顶层const

指针本身是一个对象,可以指向另一个对象,指针本身是常量以及指针所指的是不是一个常量是两个相互独立的问题。

顶层const表示指针本身是个常量。

底层const表示指针所指的对象是一个常量(指针的类型包含const)。

拷贝操作时,顶层const和底层const的区别:

  1. 顶层const不受什么影响
  2. 底层const存在限制,要么拷入拷出的对象都是具有底层const资格,要么能够转换:非常量可以转常量,反之不行

2.4.4 constexpr和常量表达式

常量表达式:值不会发生改变并且在编译过程就能够得到计算结果的表达式。字面值属于常量表达式,用常量表达式初始化的const对象也是常量表达式。

在一个复杂的系统,很难分辨一个初始值到底是不是常量表达式,C++11新标准规定,允许变量声明为constexpr类型,以便由编译器来验证变量的值是否是一个常量表达式。

声明为constexpr的变量一定是一个常量,而且必须用常量表达式初始化。

尽管普通函数不能作为constexpr变量的初始值,新标准允许定义一种特殊的constexpr函数,这种函数应该足够简单以使得编译时就可以计算出结果,这样就能用constexpr函数去初始化constexpr变量了。

常量表达式需要编译时就得到计算,因此对声明constexpr时用的的类型必须有所限制,这些类型一般比较简单,值也显而易见、容易得到,称为“字面值类型”。

算术类型、引用和指针都属于字面值类型,其中引用和指针受严格限制。一个constexpr指针的初始值必须是nullptr或者0,或是存储于某个固定地址中的对象。

函数体内部定义的变量一般来说并非存在固定地址,函数体之外的对象地址固定不变,函数可以定义有效范围超出函数本身的变量,这些变量也有固定地址。

在constexpr声明中如果定义了一个指针,限定符constexpr只对指针有效,与指针所指对象无关。

constexpr指针既可以指向常量也可以指向一个非常量,如果是非常量,则非常量的存储地址必须是固定的。

2.5 处理类型

2.5.1 类型别名

两种方法定义类型别名:

  1. 传统的方法是使用关键字typedef
  2. C++11新标准规定了新的方法,使用别名声明来定义类型的别名:using
typedef char *pstring;
const pstring cstr = 0;        //cstr是指向char的常量指针
const pstring *ps;            //ps是一个指针,它的对象是指向char的常量指针;

const char *cstr2 = 0;        //指向const char的指针

使用类型别名的声明语句时,尝试将类型名替换成本来的样子,以理解该语句的含义,这是错误的理解。

2.5.2 auto类型说明符

C++11新标准引入了auto类型说明符,用它能让编译器替我们分析表达式所属的类型。auto让编译器通过初始值推算变量类型,因此,auto定义的变量必须有初始值。

编译器推断出来的auto类型有时候和初始值的类型并不完全一致,编译器会适当地改变结果类型使其更复合初始化规则。

  1. 编译器以引用对象的类型作为auto的类型:

    int i = 0, &r = i;
    auto a = r;            //a是一个整数(r是i的别名,而i是一个整数)
    
  2. auto一般会忽略掉顶层const,同时底层const则会被保留下来

    const int ci = i, &cr = ci;
    auto b = ci;    //b是一个整数(ci的顶层const特性被忽略掉了)
    auto c = cr;    //c是一个整数(cr是ci的别名,ci本身是一个顶层const)
    auto d = &i;    //d是一个整型指针(整数的地址就是指向整数的指针)
    auto e = &ci;    //e是一个指向整数常量的指针(对常量对象取地址是一种底层const)
    
  3. 如果希望推断出的auto类型是一个顶层const,需要明确指出:

    const auto f = ci;    //ci的推演类型是int,f是const int
    
  4. 还可以将引用类型设为auto,此时原来的初始化规则仍然适用:

    auto &g = ci;    //g是一个整型常量引用,绑定到ci
    auto &h = 42;    //错误:不能为非常量引用绑定字面值
    const auto &j = 42;    //正确:可以为常量引用绑定字面值
    

    设置一个类型为auto的引用时,初始值中的顶层常量属性仍然保留。==和往常一样,如果我们给初始值绑定一个引用,则此时的常量就不是顶层常量了。==

    //对加粗语句的理解,auto引用
    auto &g = ci;    //g具有顶层常量,ci是常量,g也是常量
    //对高亮语句理解,普通引用
    const int &g = ci;    //g具有底层常量,只有g引用的对象是常量
    

要在一条语句中定义多个变量,符号&和*只从属于某个声明符,而非基本数据类型的一部分,因此初始值必须是同一种数据类型:

auto k = ci, &l = i;    //k是整数,l是整数引用
auto &m = ci, *p = &ci;    //m是对整型常量的引用
//错误:i的类型是int而&ci的类型是const int
auto &n = i, *p2 = &ci;

2.5.3 decltype类型指示符

C++11新标准引入类型说明符decltype:选择并返回操作数的数据类型。在此过程,编译器分析表达式并得到它的类型,却不实际计算表达式的值。

decltype处理顶层const和引用的方式和auto有些许不同。

如果decltype使用的表达式是一个变量,则decltype返回该变量的类型(包括顶层const和引用在内):

const int ci = 0, & cj = ci;
decltype(ci) x = 0;    //x的类型是const int
decltype(cj) y = x;    //y的类型是const int&, y绑定到变量x
decltype(cj) z;    //错误:z是一个引用,必须初始化

引用从来都是作为其所指对象的同义词出现,只有在decltype处是个例外。

如果decltype使用的表达式不是一个变量,则decltype返回表达式结果对应的类型。有些表达式向decltype返回一个引用类型,则该表达式的结果对象能作为一条赋值语句的左值:

int i = 42, *p = &i, &r = i;
decltype(r + 0) b;    //正确:加法的结果是int
decltype(*p) c;        //错误:c是int&,必须初始化

decltype和auto的另一处重要区别是:decltype的结果类型与表达式形式密切相关。

如果decltype使用的是一个不加括号的变量,则得到的结果就是该变量的类型;如果加上了一层或多层括号,编译器就会把它当成是一个表达式。变量是一种可以作为赋值语句左值的特殊表达式,所以这样的decltype就会得到引用类型。

decltype((i))     d;    //错误:d是int&,结果将是引用
decltype(i)     e;    //正确:e是一个int

==decltype((variable))(注意是双层括号)的结果永远是引用,而decltype(variable)结果只有当variable本身就是一个引用时才是引用==

2.6 自定义数据结构

类以关键字struct开始,紧跟类名和类体。类体由花括号包围形成了一个新的作用域,类内部定义的名字可以与类外部定义的名字重复,但是在类内必须唯一。类体右侧的表示结束的花括号后必须写一个分号,和定义变量一个道理。

类的定义和对象的定义可以放一起,但是不推荐,这样将两种不同实体的定义混在一句语句里,一会定义类,一会定义变量。

C++11新标准规定,可以为数据成员提供一个类内初始值。创建对象时,类内初始值将用于初始化数据成员。没有初始值的成员将被默认初始化。

类内初始值的限制:可以放花括号,或者放在等号右边,不能使用圆括号。

用户可以使用C++语言提供的另一个关键字class来定义自己的数据结构。

2.6.3 编写自己的头文件

函数体内部定义类,将受到一些限制。所以一般类不定义在函数体内。当在函数体外定义类时,在各个指定的源文件可能只有一处该类的定义,如果要在不同文件中使用同一个类,类的定义就必须保持一致。

为了确保各个文件中类的定义一致,类通常被定义在头文件中,而且类所在头文件的名字应与类的名字一样。

头文件通常包含那些只能被定义一次的实体,如类、const和constexpr变量等。在书写类的时候,可以通过适当处理,使得多次包含同一个头文件的情况也能安全和正常地工作。

确保头文件多次包含仍能安全工作的常用技术是预处理器,它由C++语言从C语言继承而来。

预处理器是编译之前执行的一段程序,可以部分地改变我们所写的程序。如:当预处理器看到#include标记时就会用指定的头文件的内容代替#include。

C++程序还会使用一项预处理功能是头文件保护符:依赖于预处理变量。预处理变量有两种状态:已定义和未定义。

  1. #define:指令把一个名字设为预处理变量
  2. #ifdef:当且仅当变量已定义时为真
  3. #ifndef:当且仅当变量未定义时为真
  4. 一旦检查结果为真,则执行后续操作,直至遇到#endif 指令为止
  5. 一旦检查结果不为真,编译器将忽略#ifndef(或#ifdef)到#endif之间的部分

预处理变量无视C++语言中关于作用域的规则

为了避免与程序中的其他实体发生名字冲突,一般把预处理变量的名字全部大写。